//! # get newest dst version info
//! ## By what
//! - klei's [dst_forum](https://forums.kleientertainment.com/game-updates/dst/)
//! - It would be nice if there was an *easier* way

const TARGET_URL: &str = "https://forums.kleientertainment.com/game-updates/dst/";

use anyhow::Result;
use lazy_static::lazy_static;
use reqwest::{self, Url};
use scraper::{Html, Selector};
use std::{
    fmt::Display,
    ops::{Deref, DerefMut},
};
// use time::PrimitiveDateTime;

lazy_static! {
    // Selectors
    // xpath: //li[@class="cCmsRecord_row "]
    // Three children: h3(version number), span(pin or hotfix), div(release date)
    static ref LI_SELECTOR: Selector = Selector::parse(r#"li[class="cCmsRecord_row "]"#).unwrap();
    static ref A_SELECTOR: Selector = Selector::parse(r#"a"#).unwrap();
    static ref H3_SELECTOR: Selector = Selector::parse(r#"h3"#).unwrap();
    static ref SPAN_SELECTOR: Selector = Selector::parse(r#"span"#).unwrap();
    static ref DIV_SELECTOR: Selector = Selector::parse(r#"div"#).unwrap();
    static ref I_SELECTOR: Selector = Selector::parse(r#"i"#).unwrap();
}

/// DST Version Information from dst_forum
#[derive(Debug)]
pub struct DstVersionInfo {
    /// Version number
    number: usize,
    /// Version Type
    vtype: DstVersionType,
    /// Release Date
    vdate: String,
    /// Corresponding url
    url: Url,

    /// is hotfix
    is_hotfix: bool,
}

impl DstVersionInfo {
    pub fn new(
        number: usize,
        vtype: DstVersionType,
        vdate: String,
        is_hotfix: bool,
        url: Url,
    ) -> Self {
        Self {
            number,
            vtype,
            vdate,
            url,
            is_hotfix,
        }
    }

    /// Version number
    pub fn number(&self) -> &usize {
        &self.number
    }

    /// Version Type
    pub fn vtype(&self) -> &DstVersionType {
        &self.vtype
    }

    /// Release Date
    pub fn vdate(&self) -> &str {
        &self.vdate
    }

    /// is hotfix
    pub fn is_hotfix(&self) -> &bool {
        &self.is_hotfix
    }

    /// Corresponding url
    pub fn url(&self) -> &Url {
        &self.url
    }
}

impl Display for DstVersionInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "DstVersionInfo[{}-{}-{}]",
            self.number, self.vtype, self.vdate
        )
    }
}

/// DST Version Type
#[derive(Debug)]
pub enum DstVersionType {
    RELEASE,
    TEST,
    UNKNOW,
}

impl Display for DstVersionType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::RELEASE => "Release",
                Self::TEST => "Test",
                Self::UNKNOW => "Unknow",
            }
        )
    }
}

/// **TODO**: set a update time
///
/// Store all version information
#[derive(Debug)]
pub struct DstVersionStore(Vec<DstVersionInfo>);

impl DstVersionStore {
    /// get a new `DstVersionStore`
    ///
    /// `versions_vec` will **not be sorted**
    ///
    /// you must use `sort_by_new_to_old(&mut self)` first
    pub fn new() -> Self {
        Self(vec![])
    }

    /// update the dstversion info vec
    ///
    /// It will be sorted
    pub async fn update(&mut self) -> Result<()> {
        // get klei's [dst_forum](https://forums.kleientertainment.com/game-updates/dst/)
        let document = reqwest::get(TARGET_URL).await?.text().await?;

        // parse the document
        let html = Html::parse_document(&document);

        // Find the parent of these three parts
        let lis = html.select(&LI_SELECTOR);

        // Three children: h3(version number), span(pin or hotfix or don't have it), div(release date)
        let mut result = vec![];
        for li in lis {
            // get li_a element
            let Some(li_a_ele) = li.select(&A_SELECTOR).next() else {
                log::warn!("Unrecognized version information: li_a");
                continue;
            };

            // get h3 element version and number
            let Some(h3_ele) = li_a_ele.select(&H3_SELECTOR).next() else {
                log::warn!("Unable to get version number information li_a_h3");
                continue;
            };

            // get ["527422", "Release"] from h3
            let h3_text: String = h3_ele.text().collect();
            let h3_text = h3_text
                .split('\n')
                .into_iter()
                .filter_map(|e| {
                    let e = e.trim();
                    if e.is_empty() {
                        None
                    } else {
                        Some(e)
                    }
                })
                .collect::<Vec<_>>();

            // if h3_text.len() == 0 {
            //     log::warn!("can't get version number");
            //     continue;
            // }
            // log::debug!("h3 content: {:?}", h3_text);

            // span(pin or hotfix or don't have it)
            let mut is_hotfix = false;
            if let Some(span) = li_a_ele.select(&SPAN_SELECTOR).next() {
                // have a span element
                // check
                // span_i => Hotfix
                if span.select(&I_SELECTOR).next().is_some() {
                    is_hotfix = true;
                }

                // dont have span element => Unknow
                // do nothing
            };

            // log::debug!("is_hotfix: {}", is_hotfix);

            // get div info release date
            let Some(div) = li_a_ele.select(&DIV_SELECTOR).next() else {
                log::warn!("can't get release date");
                continue;
            };
            let div: String = div.text().collect();

            // log::debug!(
            //     "release date: {}",
            //     div.trim().trim_matches('.').replace("Released ", "")
            // );

            // get url
            let Some(url) = li_a_ele.value().attr("href") else {
                log::warn!("can't get url");
                continue;
            };

            let Ok(url) = Url::parse(url) else {
                log::warn!("can't parse url: {}", url);
                continue;
            };

            // log::debug!("get url: {}", url);

            let Some(number) = h3_text.first() else {
                log::warn!("can't get version number");
                continue;
            };
            let Ok(number) = number.parse::<usize>() else {
                log::warn!("can't parse a version number: {}", number);
                continue;
            };

            // get version type
            let vtype = match h3_text.get(1) {
                Some(s) => {
                    if s.contains("Release") {
                        DstVersionType::RELEASE
                    } else if s.contains("Test") {
                        DstVersionType::TEST
                    } else {
                        DstVersionType::UNKNOW
                    }
                }
                None => DstVersionType::UNKNOW,
            };

            let vdate: String = div.trim().trim_matches('.').replace("Released ", "");

            let dstvinfo = DstVersionInfo::new(number, vtype, vdate, is_hotfix, url);

            result.push(dstvinfo);
        }
        // update self
        self.0 = result;
        self.sort_by_new_to_old();
        Ok(())
    }

    /// Sort by new to old
    pub fn sort_by_new_to_old(&mut self) {
        self.sort_by_key(|ele| ele.number);
        self.reverse();
    }

    /// get newest version
    ///
    /// (release, unknow, test)
    pub fn newest_version(&self) -> [Option<&DstVersionInfo>; 3] {
        let mut release = None;
        let mut unknow = None;
        let mut test = None;

        for i in self.0.iter() {
            match i.vtype {
                DstVersionType::RELEASE => {
                    if release.is_none() {
                        release = Some(i);
                    }
                }
                DstVersionType::TEST => {
                    if test.is_none() {
                        test = Some(i);
                    }
                }
                DstVersionType::UNKNOW => {
                    if unknow.is_none() {
                        unknow = Some(i);
                    }
                }
            }

            if release.is_some() && test.is_some() && unknow.is_some() {
                break;
            }
        }

        if let Some(in_release) = release {
            if let Some(in_unknow) = unknow {
                if in_release.number() >= in_unknow.number() {
                    unknow = None;
                }
            }
        }

        [release, unknow, test]
    }
}

impl Default for DstVersionStore {
    fn default() -> Self {
        Self::new()
    }
}

impl From<Vec<DstVersionInfo>> for DstVersionStore {
    fn from(versions: Vec<DstVersionInfo>) -> Self {
        Self(versions)
    }
}

impl Deref for DstVersionStore {
    type Target = Vec<DstVersionInfo>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for DstVersionStore {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}
